package com.mixpanel.android.viewcrawler;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.DisplayMetrics;
import android.util.JsonWriter;
import android.util.Log;
import android.util.LruCache;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import com.mixpanel.android.mpmetrics.MPConfig;
import com.mixpanel.android.mpmetrics.ResourceIds;
import com.mixpanel.android.util.L;
import com.mixpanel.android.util.MPLog;

import org.json.JSONArray;
import org.json.JSONObject;

@TargetApi(MPConfig.UI_FEATURES_MIN_API)
        /* package */ class ViewSnapshot {

    private static final int MAX_CLASS_NAME_CACHE_SIZE = 255;
    @SuppressWarnings("unused")
    private static final String LOGTAG = "MixpanelAPI.Snapshot";
    private final MPConfig mConfig;
    private final RootViewFinder mRootViewFinder;
    private final List<PropertyDescription> mProperties;
    private final ClassNameCache mClassnameCache;
    private final Handler mMainThreadHandler;
    private final ResourceIds mResourceIds;

    public ViewSnapshot(Context context, List<PropertyDescription> properties, ResourceIds resourceIds) {
        mConfig = MPConfig.getInstance(context);
        mProperties = properties;
        mResourceIds = resourceIds;
        mMainThreadHandler = new Handler(Looper.getMainLooper());
        mRootViewFinder = new RootViewFinder();
        mClassnameCache = new ClassNameCache(MAX_CLASS_NAME_CACHE_SIZE);
    }

    /**
     * Take a snapshot of each activity in liveActivities. The given UIThreadSet will be accessed
     * on the main UI thread, and should contain a set with elements for every activity to be
     * snapshotted. Given stream out will be written on the calling thread.
     */
    public void snapshots(UIThreadSet<Activity> liveActivities, OutputStream out, JSONArray activities)
            throws IOException {
        mRootViewFinder.findInActivities(liveActivities);
        final FutureTask<List<RootViewInfo>> infoFuture = new FutureTask<List<RootViewInfo>>(mRootViewFinder);
        mMainThreadHandler.post(infoFuture);

        final OutputStreamWriter writer = new OutputStreamWriter(out);
        List<RootViewInfo> infoList = Collections.<RootViewInfo>emptyList();
        writer.write("[");

        try {
            infoList = infoFuture.get(1, TimeUnit.SECONDS);
        } catch (final InterruptedException e) {
            MPLog.d(LOGTAG, "Screenshot interrupted, no screenshot will be sent.", e);
        } catch (final TimeoutException e) {
            MPLog.i(LOGTAG,
                    "Screenshot took more than 1 second to be scheduled and executed. No screenshot will be sent.", e);
        } catch (final ExecutionException e) {
            MPLog.e(LOGTAG, "Exception thrown during screenshot attempt", e);
        }

        final int infoCount = infoList.size();
        for (int i = 0; i < infoCount; i++) {
            try {
                if (i > 0) {
                    writer.write(",");
                }
                //for debug....
                JSONObject activity = new JSONObject();

                final RootViewInfo info = infoList.get(i);
                writer.write("{");
                writer.write("\"activity\":");
                writer.write(JSONObject.quote(info.activityName));

                writer.write(",");
                writer.write("\"scale\":");
                writer.write(String.format("%s", info.scale));

                //for debug....
                activity.put("activity", JSONObject.quote(info.activityName));
                activity.put("scale", String.format("%s", info.scale));

                writer.write(",");
                writer.write("\"serialized_objects\":");
                {
                    JSONObject serialized_objects = new JSONObject();
                    final JsonWriter j = new JsonWriter(writer);
                    j.beginObject();
                    j.name("rootObject").value(info.rootView.hashCode());
                    //for debug....
                    serialized_objects.put("rootObject", info.rootView.hashCode());

                    JSONArray objects = new JSONArray();
                    j.name("objects");
                    snapshotViewHierarchy(j, info.rootView, objects);
                    //for debug....
                    serialized_objects.put("objects", objects);
                    j.endObject();
                    j.flush();
                    //for debug....
                    activity.put("serialized_objects", serialized_objects);
                }
                writer.write(",");
                writer.write("\"screenshot\":");
                writer.flush();
                info.screenshot.writeBitmapJSON(Bitmap.CompressFormat.PNG, 100, out, activity);
                writer.write("}");
                //for debug....
                activities.put(activity);
            } catch (Exception e) {
                L.e(e);
            }
        }

        writer.write("]");
        writer.flush();
    }

    // For testing only
    /* package */ List<PropertyDescription> getProperties() {
        return mProperties;
    }

    /* package */ void snapshotViewHierarchy(JsonWriter j, View rootView, JSONArray arr)
            throws IOException {
        j.beginArray();
        snapshotView(j, rootView, arr);
        j.endArray();
    }

    private void snapshotView(JsonWriter j, View view, JSONArray arr)
            throws IOException {
        try {
            if (view.getVisibility() == View.INVISIBLE && mConfig.getIgnoreInvisibleViewsEditor()) {
                L.d("ViewSnapshot.snapshotView visibility:" + view.getVisibility() + " ;igonneEditor:" + mConfig
                        .getIgnoreInvisibleViewsEditor() + " ; will return");
                return;
            }

            final int viewId = view.getId();
            final String viewIdName;
            if (-1 == viewId) {
                viewIdName = null;
            } else {
                viewIdName = mResourceIds.nameForId(viewId);
            }

            Class<?> dd = view.getClass();
//            //TextView 时候获取内容
//            //L.d("*************************>" + klass.getName());
////            if ("android.widget.Button".equals(dd.getName())) {
////                Button tv = (Button) view;
////                if ("无IDButton".equals(tv.getText())) {
////                    L.d("******>" + tv.getText());
////                }
////            }


            if (WebView.class.getName().equals(dd.getName())) {
                L.i("包含一个webview");
            } else if (ScrollView.class.getName().equals(dd.getName())) {
                L.i("包含一个ScrollView");
            }
            j.beginObject();
            j.name("hashCode").value(view.hashCode());
            j.name("id").value(viewId);
            j.name("mp_id_name").value(viewIdName);

            //for debug...........
            JSONObject obj = new JSONObject();
            obj.put("hashCode", view.hashCode());
            obj.put("id", viewId);
            obj.put("mp_id_name", TextUtils.isEmpty(viewIdName) ? "" : viewIdName);

            final CharSequence description = view.getContentDescription();
            if (null == description) {
                j.name("contentDescription").nullValue();
                //for debug...........
                obj.put("contentDescription", "");
            } else {
                j.name("contentDescription").value(description.toString());
                //for debug...........
                obj.put("contentDescription", description.toString());
            }

            final Object tag = view.getTag();
            if (null == tag) {
                j.name("tag").nullValue();
                //for debug...........
                obj.put("tag", "");
            } else if (tag instanceof CharSequence) {
                j.name("tag").value(tag.toString());
                //for debug...........
                obj.put("tag", tag.toString());
            }

            j.name("top").value(view.getTop());
            j.name("left").value(view.getLeft());
            j.name("width").value(view.getWidth());
            j.name("height").value(view.getHeight());
            j.name("scrollX").value(view.getScrollX());
            j.name("scrollY").value(view.getScrollY());
            j.name("visibility").value(view.getVisibility());
//            Log.i("vv", "解析组件ing--viewgetWidth:" + view.getWidth()+"---height:"+view.getHeight());
            //for debug...........
            obj.put("top", view.getTop());
            obj.put("left", view.getLeft());
            obj.put("width", view.getWidth());
            obj.put("height", view.getHeight());
            obj.put("scrollX", view.getScrollX());
            obj.put("scrollY", view.getScrollY());
            obj.put("visibility", view.getVisibility());

            /**
             * 添加绝对坐标
             */
            int[] outLocale = new int[2];
            //相对整个屏幕的绝对坐标(包含状态栏)
            view.getLocationOnScreen(outLocale);
            //相对window的绝对坐标.(不包含任务栏)
            //view.getLocationInWindow(outLocale);
            if (outLocale.length == 2) {
                j.name("atop").value(outLocale[1]);
                j.name("aleft").value(outLocale[0]);
                obj.put("aleft", outLocale[0]);
                obj.put("atop", outLocale[1]);
            }

            float translationX = 0;
            float translationY = 0;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                translationX = view.getTranslationX();
                translationY = view.getTranslationY();
            }

            j.name("translationX").value(translationX);
            j.name("translationY").value(translationY);
            //for debug...........
            obj.put("translationX", translationX);
            obj.put("translationY", translationY);
            JSONArray classes = new JSONArray();

            j.name("classes");
            j.beginArray();
            Class<?> klass = view.getClass();
            //TextView 时候获取内容
            //L.d("*************************>" + klass.getName());
            if ("android.widget.TextView".equals(klass.getName())) {
                TextView tv = (TextView) view;
                L.d("******>" + tv.getText());
            }
            do {
                j.value(mClassnameCache.get(klass));
                //for debug...........
                classes.put(mClassnameCache.get(klass));
                klass = klass.getSuperclass();
            } while (klass != Object.class && klass != null);
            j.endArray();
            //for debug...........
            obj.put("classes", classes);

            addProperties(j, view, obj);

            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
            if (layoutParams instanceof RelativeLayout.LayoutParams) {
                RelativeLayout.LayoutParams relativeLayoutParams = (RelativeLayout.LayoutParams) layoutParams;
                int[] rules = relativeLayoutParams.getRules();

                //for debug.........
                JSONArray layoutRules = new JSONArray();
                j.name("layoutRules");
                j.beginArray();
                for (int rule : rules) {
                    j.value(rule);
                    layoutRules.put(rule);
                }
                j.endArray();
                obj.put("layoutRules", layoutRules);
            }

            //for debug.........
            JSONArray subviews = new JSONArray();
            j.name("subviews");
            j.beginArray();
            if (view instanceof ViewGroup) {
                final ViewGroup group = (ViewGroup) view;
                final int childCount = group.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    final View child = group.getChildAt(i);
                    // child can be null when views are getting disposed.
                    if (null != child) {
                        j.value(child.hashCode());
                        subviews.put(child.hashCode());
                    }
                }
            }
            j.endArray();
            j.endObject();

            obj.put("subviews", subviews);
            arr.put(obj);

            if (view instanceof ViewGroup) {
                final ViewGroup group = (ViewGroup) view;
                final int childCount = group.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    final View child = group.getChildAt(i);
                    // child can be null when views are getting disposed.
                    if (null != child) {
                        snapshotView(j, child, arr);
                    }
                }
            }
        } catch (Throwable e) {
            L.e(e);
        }
    }

    private void addProperties(JsonWriter j, View v, JSONObject obj) {
        try {
            final Class<?> viewClass = v.getClass();
            //L.d("addProperties  mProperties:" + mProperties.toString());
            //进入前是真,有假的值就为假
            boolean processable = true;
            int x = 0;
            for (final PropertyDescription desc : mProperties) {
                x += 1;
                if (desc.targetClass.isAssignableFrom(viewClass) && null != desc.accessor) {
                    final Object value = desc.accessor.applyMethod(v);
                    //MPLog.i(desc.name + "<====>" + value + "==>" + processable);

                    if (processable) {
                        if (!TextUtils.isEmpty(desc.name)) {
                            if ("clickable".equals(desc.name)) {
                                boolean isT = (Boolean) value;
                                if (!isT) {
                                    processable = false;
                                }
                            } else if ("alpha".equals(desc.name)) {
                                float alpha = (float) value;
                                //MPLog.e("预判断  alpha " + value + " ====!=0: " + (alpha == 0));
                                //透明度是0则不可见
                                if (alpha == 0) {
                                    processable = false;
                                }
                            } else if ("hidden".equals(desc.name)) {
                                int hide = (int) value;
                                //MPLog.e("预判断  hidden " + value + " ====!=0: " + (hide != 0));
                                //hidden是0则隐藏
                                if (hide != 0) {
                                    processable = false;
                                }
                            }
                        }
                    }
                    //MPLog.e("结果  processable " + processable);
                    j.name("processable").value(processable ? "1" : "0");
                    //for debug......
                    obj.put("processable", processable ? 1 : 0);

                    if (null == value) {
                        // Don't produce anything in this case
                    } else if (value instanceof Number) {
                        j.name(desc.name).value((Number) value);
                        //for debug......
                        obj.put(desc.name, (Number) value);
                    } else if (value instanceof Boolean) {
                        j.name(desc.name).value((Boolean) value);
                        //for debug......
                        obj.put(desc.name, (Boolean) value);
                    } else if (value instanceof ColorStateList) {
                        j.name(desc.name).value((Integer) ((ColorStateList) value).getDefaultColor());
                        //for debug......
                        obj.put(desc.name, (Integer) ((ColorStateList) value).getDefaultColor());
                    } else if (value instanceof Drawable) {
                        final Drawable drawable = (Drawable) value;
                        final Rect bounds = drawable.getBounds();
                        JSONObject dn = new JSONObject();
                        j.name(desc.name);
                        //for debug.....
                        j.beginObject();
                        j.name("classes");
                        j.beginArray();
                        //for debug.....
                        JSONArray classes = new JSONArray();

                        Class klass = drawable.getClass();
                        while (klass != Object.class) {
                            j.value(klass.getCanonicalName());
                            //for debug....
                            classes.put(klass.getCanonicalName());
                            klass = klass.getSuperclass();
                        }
                        j.endArray();

                        dn.put("classes", classes);
                        JSONObject dimensions = new JSONObject();
                        j.name("dimensions");
                        j.beginObject();
                        j.name("left").value(bounds.left);
                        j.name("right").value(bounds.right);
                        j.name("top").value(bounds.top);
                        j.name("bottom").value(bounds.bottom);
                        //for debug......
                        dimensions.put("left", bounds.left);
                        dimensions.put("right", bounds.right);
                        dimensions.put("top", bounds.top);
                        dimensions.put("bottom", bounds.bottom);
                        dn.put("dimensions", dimensions);
                        j.endObject();
                        if (drawable instanceof ColorDrawable) {
                            final ColorDrawable colorDrawable = (ColorDrawable) drawable;
                            j.name("color").value(colorDrawable.getColor());
                            obj.put("color", colorDrawable.getColor());
                        }
                        j.endObject();
                        obj.put(desc.name, dn);
                    } else {
                        j.name(desc.name).value(value.toString());
                        //for debug......
                        obj.put(desc.name, value.toString());
                    }
                }
            }
        } catch (
                Exception e) {
            L.e(e);
        }

    }

    private static class ClassNameCache extends LruCache<Class<?>, String> {
        public ClassNameCache(int maxSize) {
            super(maxSize);
        }

        @Override
        protected String create(Class<?> klass) {
            return klass.getCanonicalName();
        }
    }

    private static class RootViewFinder implements Callable<List<RootViewInfo>> {
        private final List<RootViewInfo> mRootViews;
        private final DisplayMetrics mDisplayMetrics;
        private final CachedBitmap mCachedBitmap;
        // 系统的dpi
        private final int mClientDensity = DisplayMetrics.DENSITY_DEFAULT;
        private UIThreadSet<Activity> mLiveActivities;
        public RootViewFinder() {
            mDisplayMetrics = new DisplayMetrics();
            mRootViews = new ArrayList<RootViewInfo>();
            mCachedBitmap = new CachedBitmap();
        }

        public void findInActivities(UIThreadSet<Activity> liveActivities) {
            mLiveActivities = liveActivities;
        }

        @Override
        public List<RootViewInfo> call() throws Exception {
            mRootViews.clear();
            final Set<Activity> liveActivities = mLiveActivities.getAll();

            for (final Activity a : liveActivities) {
                final String activityName = a.getClass().getCanonicalName();
                final View rootView = a.getWindow().getDecorView().getRootView();
                a.getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics);
                final RootViewInfo info = new RootViewInfo(activityName, rootView);
                mRootViews.add(info);
            }

            final int viewCount = mRootViews.size();
            for (int i = 0; i < viewCount; i++) {
                final RootViewInfo info = mRootViews.get(i);
                L.w("==================> will takeScreenshot");
                takeScreenshot(info);
            }

            return mRootViews;
        }

        private void takeScreenshot(final RootViewInfo info) {
            final View rootView = info.rootView;
            Bitmap rawBitmap = null;

            try {
                final Method createSnapshot = View.class.getDeclaredMethod("createSnapshot", Bitmap.Config.class, Integer.TYPE, Boolean.TYPE);
                createSnapshot.setAccessible(true);
                rawBitmap = (Bitmap) createSnapshot.invoke(rootView, Bitmap.Config.RGB_565, Color.WHITE, false);
            } catch (final NoSuchMethodException e) {
                MPLog.v(LOGTAG, "Can't call createSnapshot, will use drawCache", e);
            } catch (final IllegalArgumentException e) {
                MPLog.d(LOGTAG, "Can't call createSnapshot with arguments", e);
            } catch (final InvocationTargetException e) {
                MPLog.e(LOGTAG, "Exception when calling createSnapshot", e);
            } catch (final IllegalAccessException e) {
                MPLog.e(LOGTAG, "Can't access createSnapshot, using drawCache", e);
            } catch (final ClassCastException e) {
                MPLog.e(LOGTAG, "createSnapshot didn't return a bitmap?", e);
            }

            Boolean originalCacheState = null;
            try {
                if (null == rawBitmap) {
                    originalCacheState = rootView.isDrawingCacheEnabled();
                    rootView.setDrawingCacheEnabled(true);
                    rootView.buildDrawingCache(true);
                    rawBitmap = rootView.getDrawingCache();
                }
            } catch (final RuntimeException e) {
                MPLog.v(LOGTAG, "Can't take a bitmap snapshot of view " + rootView + ", skipping for now.", e);
            }

            //个人理解像素比. 取值是系统的dpi/截图像素
            float scale = 1.0f;
            if (null != rawBitmap) {
                final int rawDensity = rawBitmap.getDensity();

                if (rawDensity != Bitmap.DENSITY_NONE) {
                    scale = ((float) mClientDensity) / rawDensity;
                }

                final int rawWidth = rawBitmap.getWidth();
                final int rawHeight = rawBitmap.getHeight();
                final int destWidth = (int) ((rawBitmap.getWidth() * scale) + 0.5);
                final int destHeight = (int) ((rawBitmap.getHeight() * scale) + 0.5);

                Log.i("vv", "解析图片前:rawWidth[" + rawWidth + "] rawHeight[" + rawHeight + "]  处理图片后：destWidth:[" + destWidth + "] destHeight[" + destHeight + "]");
                if (rawWidth > 0 && rawHeight > 0 && destWidth > 0 && destHeight > 0) {
                    mCachedBitmap.recreate(destWidth, destHeight, mClientDensity, rawBitmap);
                }
            }

            if (null != originalCacheState && !originalCacheState) {
                rootView.setDrawingCacheEnabled(false);
            }
            info.scale = scale;
            info.screenshot = mCachedBitmap;
        }
    }

    private static class CachedBitmap {
        private final Paint mPaint;
        private Bitmap mCached;

        public CachedBitmap() {
            mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
            mCached = null;
        }

        public synchronized void recreate(int width, int height, int destDensity, Bitmap source) {
            if (null == mCached || mCached.getWidth() != width || mCached.getHeight() != height) {
                try {
                    mCached = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
                } catch (final OutOfMemoryError e) {
                    mCached = null;
                }

                if (null != mCached) {
                    mCached.setDensity(destDensity);
                }
            }

            if (null != mCached) {
                final Canvas scaledCanvas = new Canvas(mCached);
                scaledCanvas.drawBitmap(source, 0, 0, mPaint);
            }
        }

        // Writes a QUOTED base64 string (or the string null) to the output stream
        public synchronized void writeBitmapJSON(CompressFormat format, int quality, OutputStream out, JSONObject activity) {
            try {
                if (null == mCached || mCached.getWidth() == 0 || mCached.getHeight() == 0) {
                    out.write("null".getBytes());
                } else {
                    out.write('"');
                    final Base64OutputStream imageOut = new Base64OutputStream(out, Base64.NO_WRAP);
//                    mCached.compress(Bitmap.CompressFormat.PNG, 100, imageOut);
                    mCached.compress(Bitmap.CompressFormat.JPEG, 80, imageOut);

                    activity.put("screenshot", imageOut.toString());
                    imageOut.flush();
                    out.write('"');

                }
            } catch (Exception e) {
                L.e(e);
            }
        }
    }

    private static class RootViewInfo {
        public final String activityName;
        public final View rootView;
        public CachedBitmap screenshot;
        public float scale;
        public RootViewInfo(String activityName, View rootView) {
            this.activityName = activityName;
            this.rootView = rootView;
            this.screenshot = null;
            this.scale = 1.0f;
        }
    }
}
